package com.google.bitcoin.examples;

import com.google.bitcoin.core.*;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.apache.commons.lang.exception.ExceptionUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.util.Map;

/**
 * @author apetersson
 */
public class MultisigHttpServer {

    private final Map<Address, ECKey> myKeys = Maps.newConcurrentMap(); // in reality, this is a database of sorts..
    private final int port;
    private final NetworkParameters networkParameters;
    private HttpServer server;

    public MultisigHttpServer(int port, NetworkParameters networkParameters) {
        this.port = port;
        this.networkParameters = networkParameters;
    }

    public static void main(String[] args) throws Exception {
        new MultisigHttpServer(8000, NetworkParameters.unitTests()).start();
    }

    public void start() throws IOException {
        Preconditions.checkState(server == null);
        server = HttpServer.create(new InetSocketAddress(port), 0);
        server.createContext("/add", new AddKeyHandler());
        server.createContext("/canSign", new CanSignHandler());
        server.createContext("/sign", new SignHandler());
        server.setExecutor(null); // creates a default executor
        server.start();
        printDebugHelp();

    }

    private void printDebugHelp() {
        ECKey secretPrivKey = new ECKey();
        String prefix = "localhost:" + port + "/";
        System.out.println("try to\n"
                + prefix + "add?" + Utils.bytesToHexString(secretPrivKey.getPrivKeyBytes()) + "\n"
                + prefix + "canSign?" + secretPrivKey.toAddress(networkParameters) + "\n"
                + prefix + "sign (use http post with Json data of MultisigRequest)");
    }

    public void stop() {
        server.stop(0);
        server = null;
        System.out.println("shutting down signing server..");
    }

    class AddKeyHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            try {
                System.out.println(t.getRequestURI());
                ECKey privateKey = getEckeyFromString(t.getRequestURI().getQuery());
                Address address = privateKey.toAddress(networkParameters);
                myKeys.put(address, privateKey);
                writeOkMessage(t, "stored private key with address " + address);
            } catch (RuntimeException e) {
                writeErrorMessage(t, e);
            }
        }
    }

    private ECKey getEckeyFromString(String encodedPrivKey) {
        return new ECKey(new BigInteger(encodedPrivKey, 16));
    }

    class CanSignHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            try {
                System.out.println(t.getRequestURI());
                Address address = new Address(networkParameters, t.getRequestURI().getQuery());
                writeOkMessage(t, myKeys.containsKey(address) ? "true" : "false");
            } catch (RuntimeException e) {
                writeErrorMessage(t, e);
            } catch (WrongNetworkException e) {
                writeErrorMessage(t, e);
            } catch (AddressFormatException e) {
                writeErrorMessage(t, e);
            }
        }
    }

    class SignHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            try {
                MultisigRequest request = readPostData(t);
                PubKey pubKey = request.pubKeyToUse;
                ECKey keyForSigning = myKeys.get(pubKey.toAddress(networkParameters));
                if (keyForSigning == null) {
                    writeErrorMessage(t, "i do not possess this private key for " + pubKey);
                    return;
                }

                /* alternative serialisation needed?
                shall we maintain our own separate blockchain since this refers typically to an old TX?
                note, in reality a signing server would at this point check the given data against his own local blockchain.
                you should not really rely on this given connectedPubKeyScript data
                rather load connectedPubKeyScript from the blockchain.
                */
                Transaction transaction = getTransaction(request);
                byte[] connectedPubKeyScript = request.connectedPubKeyScript;
                Sha256Hash hash = transaction.hashTransactionForSignature(request.inputIndex, connectedPubKeyScript, Transaction.SigHash.ALL, false);
                if (!hash.equals(request.hashToSign)) {
                    writeErrorMessage(t, "Hash " + request.hashToSign + " not found in given transaction");
                    return;
                }
                Script script = new Script(networkParameters, connectedPubKeyScript, 0, connectedPubKeyScript.length);
                if (!script.isSentToMultisig()) {
                    writeErrorMessage(t, "this is no multisig script");
                    return;
                }
                MultisigPubkeys mutiSigs = script.getMultisigAddresses();
                if (!mutiSigs.containsPubkey(pubKey)) {
                    writeErrorMessage(t, "unable to sign, since the requested address was not part of the MultisigPubkeys" + pubKey);
                    return;
                }

                //after all the error checking do the actual work..
                byte[] response = keyForSigning.sign(request.hashToSign.getBytes());
                writeOkMessage(t, response);
            } catch (RuntimeException e) {
                writeErrorMessage(t, e);
            } catch (ScriptException e) {
                writeErrorMessage(t, e);
            } catch (ProtocolException e) {
                writeErrorMessage(t, e);
            }
        }
    }

    private Transaction getTransaction(MultisigRequest request) throws ProtocolException, IOException {
        BitcoinSerializer serializer = new BitcoinSerializer(networkParameters, true);
        ByteArrayInputStream in = new ByteArrayInputStream(request.transactionBytes);
        return (Transaction) serializer.deserialize(in);
    }

    private MultisigRequest readPostData(HttpExchange t) {
        InputStreamReader inputAsString = new InputStreamReader(t.getRequestBody(), Charsets.UTF_8);
        return new Gson().fromJson(inputAsString, MultisigRequest.class);
    }

    private void writeErrorMessage(HttpExchange t, Exception e) throws IOException {
        String fullError = ExceptionUtils.getFullStackTrace(e);
        System.err.println(fullError);
        writeErrorMessage(t, fullError);
    }

    private void writeErrorMessage(HttpExchange t, String response) throws IOException {
        writeMsg(t, response, 500);
    }

    private void writeOkMessage(HttpExchange t, String response) throws IOException {
        writeMsg(t, response, 200);
    }

    private void writeOkMessage(HttpExchange t, byte[] response) throws IOException {
        t.sendResponseHeaders(200, response.length);
        OutputStream os = t.getResponseBody();
        os.write(response);
        os.close();
    }

    private void writeMsg(HttpExchange t, String response, int statusCode) throws IOException {
        t.sendResponseHeaders(statusCode, response.length());
        OutputStream os = t.getResponseBody();
        os.write(response.getBytes(Charsets.UTF_8));
        os.close();
    }

}
